Creating and Managing Helm Charts
A Helm Chart is the standard way to package, configure, and deploy Kubernetes applications. This document explains the essential components and practices for creating custom Helm Charts.
What is a Helm Chart?
A Helm Chart is a collection of template files that define Kubernetes resources. It provides several benefits:
- Reusability: Easily deploy the same application across different environments
- Version Control: Track application versions
- Configuration Management: Apply different configurations per environment
- Dependency Management: Define dependencies on other Charts
Basic Helm Chart Structure
mychart/
├── Chart.yaml # Chart metadata
├── values.yaml # Default configuration values
├── charts/ # Dependent charts
├── templates/ # Kubernetes manifest templates
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # Template helper functions
│ └── NOTES.txt # Post-installation notes
└── .helmignore # Files to exclude from packaging
Chart.yaml - Chart Metadata
Chart.yaml defines the basic information about the Chart.
apiVersion: v2
name: myapp
description: A Helm chart for custom application
type: application
# Chart version (version of the Chart itself)
version: 1.0.0
# Application version
appVersion: "2.1.0"
# Keywords (for search)
keywords:
- web
- api
- microservice
# Maintainer information
maintainers:
- name: Developer Name
email: developer@example.com
url: https://example.com
# Homepage URL
home: https://github.com/example/myapp
# Source code repositories
sources:
- https://github.com/example/myapp
# Chart dependencies
dependencies:
- name: postgresql
version: "12.1.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "17.3.0"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
# Icon URL
icon: https://example.com/icon.png
# If deprecated
# deprecated: true
# KubeVersion constraints
kubeVersion: ">=1.24.0-0"
Important Chart.yaml Fields
| Field | Required | Description |
|---|---|---|
apiVersion | ✓ | Chart API version (v2 recommended) |
name | ✓ | Chart name (must match directory name) |
version | ✓ | Chart version (SemVer) |
appVersion | Version of the deployed application | |
type | Either application or library | |
dependencies | List of dependent charts |
values.yaml - Default Configuration Values
values.yaml defines the default values used in templates.
# Number of replicas
replicaCount: 3
# Docker image configuration
image:
repository: myregistry.azurecr.io/myapp
pullPolicy: IfNotPresent
tag: "2.1.0"
# Image pull secrets
imagePullSecrets:
- name: acr-secret
# Service account
serviceAccount:
create: true
annotations: {}
name: ""
# Pod annotations
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
# Pod security context
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Container security context
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
# Service configuration
service:
type: ClusterIP
port: 80
targetPort: 8080
annotations: {}
# Ingress configuration
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- myapp.example.com
# Resource limits
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
# Autoscaling
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
# Health checks
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
# Environment variables
env:
- name: APP_ENV
value: "production"
- name: LOG_LEVEL
value: "info"
# Environment variables from ConfigMap
envFrom:
- configMapRef:
name: myapp-config
# Secrets
secrets:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
# Volumes
volumes:
- name: config
configMap:
name: myapp-config
- name: cache
emptyDir: {}
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: cache
mountPath: /tmp/cache
# Node selector
nodeSelector: {}
# Tolerations
tolerations: []
# Affinity
affinity: {}
Template Files
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "myapp.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.env }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "myapp.selectorLabels" . | nindent 4 }}
ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
_helpers.tpl - Helper Templates
_helpers.tpl defines reusable template functions. This file is not rendered as a Kubernetes resource.
{{/*
Expand the name of the chart
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a fully qualified app name
Truncated at 63 chars because some Kubernetes name fields are limited
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Environment variables template
*/}}
{{- define "myapp.env" -}}
- name: APP_NAME
value: {{ include "myapp.fullname" . }}
- name: APP_VERSION
value: {{ .Chart.AppVersion | quote }}
- name: RELEASE_NAME
value: {{ .Release.Name }}
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- end }}
{{/*
Conditional labels
*/}}
{{- define "myapp.podLabels" -}}
{{- if .Values.podLabels }}
{{- toYaml .Values.podLabels }}
{{- end }}
{{- end }}
Key Uses of Helper Functions
- Naming Conventions: Generate resource names consistently
- Label Standardization: Follow Kubernetes best practices for labels
- Conditional Logic: Customize templates based on configuration
- Code Deduplication: Reuse common template parts
Usage Examples of Helper Functions
Defined helper functions are called from other template files using the include function.
Definition (_helpers.tpl):
{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
Usage (deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
# Call the defined function to expand the name
name: {{ include "myapp.fullname" . }}
Since the include function returns output as a string, it can be passed to other functions (like indent or nindent) via a pipeline. This is crucial for maintaining YAML indentation structure.
labels:
# Indent the output by 4 spaces
{{- include "myapp.labels" . | nindent 4 }}
Template Functions and Pipelines
Helm uses Go Templates and the Sprig function library.
Basic Template Syntax
# Value reference
{{ .Values.replicaCount }}
# Default value
{{ .Values.image.tag | default .Chart.AppVersion }}
# Conditional
{{- if .Values.ingress.enabled }}
# Create Ingress resource
{{- end }}
# Range loop
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value }}
{{- end }}
# Pipeline (chaining functions)
{{ .Values.name | upper | quote }}
# Indentation
{{- toYaml .Values.resources | nindent 12 }}
# Calling helper functions
{{ include "myapp.fullname" . }}
Commonly Used Functions
| Function | Description | Example |
|---|---|---|
default | Provide default value | {{ .Values.tag | default "latest" }} |
quote | Quote a string | {{ .Values.name | quote }} |
upper/lower | Convert case | {{ .Values.env | upper }} |
trunc | Truncate string | {{ .Values.name | trunc 63 }} |
trimSuffix | Remove suffix | {{ .Values.name | trimSuffix "-" }} |
nindent | Add indentation | {{ toYaml .Values | nindent 4 }} |
toYaml | Convert to YAML | {{ toYaml .Values.resources }} |
sha256sum | SHA256 hash | {{ .Values.config | sha256sum }} |
Chart Packaging - .tgz Files
Helm Charts are packaged as .tgz (tar.gz) archives. This file is used for publishing to Chart repositories or transferring to air-gapped environments (environments without internet access).
Filename Convention
The package filename is automatically generated based on the name and version defined in Chart.yaml.
Format: <chart-name>-<chart-version>.tgz
Example:
- Chart Name:
myapp - Version:
1.0.0 - Generated Filename:
myapp-1.0.0.tgz
Creating a Package
# Package the chart
helm package mychart/
# Output: myapp-1.0.0.tgz
Package Contents
The .tgz file contains the source files of the Chart itself, not the rendered Kubernetes manifests. During installation, Helm extracts this package and renders the templates on the fly.
# View package contents
tar -tzf myapp-1.0.0.tgz
# Output:
# myapp/Chart.yaml
# myapp/values.yaml
# myapp/templates/deployment.yaml
# myapp/templates/service.yaml
# ...
.helmignore
.helmignore specifies files to exclude from packaging.
# Development files
*.swp
*.bak
*.tmp
*.orig
*~
# Git-related
.git/
.gitignore
# CI/CD
.github/
.gitlab-ci.yml
# Tests
tests/
*.test
# Documentation
README.md
CONTRIBUTING.md
# Environment-specific files
values-dev.yaml
values-staging.yaml
Chart Development Workflow
1. Creating a Chart
# Create a new chart
helm create myapp
# Directory structure is generated
2. Developing and Testing Templates
# View rendered templates
helm template myapp ./myapp
# Use specific values file
helm template myapp ./myapp -f values-prod.yaml
# Debug mode with detailed output
helm template myapp ./myapp --debug
3. Validating the Chart
# Check chart syntax
helm lint myapp/
# Update dependencies
helm dependency update myapp/
# Dry run (doesn't actually install)
helm install myapp ./myapp --dry-run --debug
4. Packaging the Chart
# Package the chart
helm package myapp/
# Signed package (optional)
helm package myapp/ --sign --key 'keyname' --keyring path/to/keyring
5. Installing the Chart
# Install from local chart
helm install my-release ./myapp
# Install from package
helm install my-release myapp-1.0.0.tgz
# Specify custom values
helm install my-release ./myapp -f custom-values.yaml
# Override values via command line
helm install my-release ./myapp --set replicaCount=5
# Install to specific namespace
helm install my-release ./myapp -n production --create-namespace
Environment-Specific Configuration
Use different configurations for different environments (dev, staging, production).
values-dev.yaml
replicaCount: 1
image:
tag: "dev"
ingress:
enabled: false
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
env:
- name: APP_ENV
value: "development"
- name: LOG_LEVEL
value: "debug"
values-prod.yaml
replicaCount: 5
image:
tag: "2.1.0"
ingress:
enabled: true
hosts:
- host: myapp.production.com
paths:
- path: /
pathType: Prefix
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
env:
- name: APP_ENV
value: "production"
- name: LOG_LEVEL
value: "warn"
Usage
# Development environment
helm install myapp-dev ./myapp -f values-dev.yaml
# Production environment
helm install myapp-prod ./myapp -f values-prod.yaml -n production
# Merge multiple values files (later files take precedence)
helm install myapp ./myapp -f values.yaml -f values-prod.yaml -f values-override.yaml
Umbrella Chart Pattern
The Umbrella Chart is a Helm design pattern for managing multiple microservices or applications together.
Overview
An Umbrella Chart itself has few templates (templates/) and specializes in defining other Charts (sub-charts) in the dependencies section of Chart.yaml. This allows you to deploy and manage a complex system as a single release.
Key Use Cases
- Microservices Composition: Manage multiple services (Frontend, Backend, DB, etc.) collectively.
- Dependency Integration: Provide applications and necessary middleware (Redis, PostgreSQL, etc.) as a set.
Structure Example
umbrella-app/
├── Chart.yaml # Define dependencies
├── values.yaml # Global configuration (override sub-chart settings)
├── charts/ # Where dependent Charts are downloaded
└── templates/ # Usually empty, or common ConfigMap/Secret only
Chart.yaml Configuration
apiVersion: v2
name: my-complex-app
version: 1.0.0
type: application
dependencies:
- name: frontend
version: 1.2.0
repository: http://my-charts.com
- name: backend
version: 2.3.0
repository: http://my-charts.com
- name: postgresql
version: 12.1.0
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
Overriding Settings in values.yaml
You can override sub-chart settings from the parent Chart's (Umbrella Chart) values.yaml. Use the sub-chart name as the key.
# Global settings (shared across all charts)
global:
imageRegistry: myregistry.azurecr.io
# frontend sub-chart settings
frontend:
replicaCount: 3
service:
type: LoadBalancer
# backend sub-chart settings
backend:
env:
- name: DB_HOST
value: "my-complex-app-postgresql"
# postgresql sub-chart settings
postgresql:
enabled: true
auth:
database: mydb
username: myuser
Commands
To resolve dependencies and prepare the Chart, use the following commands:
# Download dependent Charts and place them in the charts/ directory
helm dependency build ./umbrella-app
# Or
helm dependency update ./umbrella-app
Publishing to Chart Repositories
Using ChartMuseum
# Upload to ChartMuseum
curl --data-binary "@myapp-1.0.0.tgz" http://chartmuseum.example.com/api/charts
# Add repository
helm repo add myrepo http://chartmuseum.example.com
helm repo update
# Install from repository
helm install my-release myrepo/myapp
Using Azure Container Registry (ACR)
# Login to ACR
az acr login --name myregistry
# Push chart as OCI artifact
helm push myapp-1.0.0.tgz oci://myregistry.azurecr.io/helm
# Install from ACR
helm install my-release oci://myregistry.azurecr.io/helm/myapp --version 1.0.0
Best Practices
1. Naming Conventions
- Chart name: lowercase, hyphen-separated (e.g.,
my-app) - Resource names: Use
{{ include "myapp.fullname" . }} - Labels: Use Kubernetes recommended labels
2. Security
# Always define security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# Set resource limits
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
3. Config and Secret Management
# Add ConfigMap hash to annotations (restart pods on config change)
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
4. Conditional Resources
# Control feature toggle via values.yaml
{{- if .Values.ingress.enabled }}
# Ingress resource
{{- end }}
5. Documentation
README.md: Overview and usage instructionsNOTES.txt: Information displayed after installationvalues.yaml: Document all configuration options with comments
Example NOTES.txt
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get all {{ .Release.Name }}
To access your application:
{{- if .Values.ingress.enabled }}
Visit: https://{{ (index .Values.ingress.hosts 0).host }}
{{- else }}
Run: kubectl port-forward svc/{{ include "myapp.fullname" . }} 8080:{{ .Values.service.port }}
Then visit: http://localhost:8080
{{- end }}
Troubleshooting
Debug Commands
# View rendered templates
helm template myapp ./myapp --debug
# Installation dry run
helm install myapp ./myapp --dry-run --debug
# Check values of installed chart
helm get values my-release
# Check installed manifests
helm get manifest my-release
# View release history
helm history my-release
Common Issues
-
Template indentation errors
- Use
nindentfunction to control indentation precisely
- Use
-
Values not applied
- Check actual values with
helm get values - Verify precedence of
--setand-f
- Check actual values with
-
Dependency issues
- Run
helm dependency update - Check
charts/directory
- Run
Summary
Key points for creating custom Helm Charts:
- Chart.yaml: Metadata and version control
- values.yaml: All configurable parameters
- templates/: Kubernetes manifest templates
- _helpers.tpl: Reusable template functions
- .tgz files: Packaged charts
- Environment-specific config: Flexible deployment across environments
Helm Charts are powerful tools that standardize and make Kubernetes application deployment reusable. Properly structured charts significantly improve team productivity.